package session

import (
	"encoding/json"
	"io/ioutil"
	"net/http"
	"os/exec"
	"path/filepath"
	"time"

	"chainmaker.org/chainmaker/smarteditor/common"
	"chainmaker.org/chainmaker/smarteditor/container"

	"chainmaker.org/chainmaker/smarteditor/conf"
	"chainmaker.org/chainmaker/smarteditor/server/pb/protogo"
	"chainmaker.org/chainmaker/smarteditor/util"
	"github.com/88250/gulu"
)

const BuildContractName = "proxyContract"
const WindowsBuildContractName = "proxyContract.exe"

const DockerMountPath = "/contract"

// invokeContractReqFormData invoke contract request data
type invokeContractReqFormData struct {
	Sid                         string            `json:"sid,omitempty"`
	ContractName                string            `json:"contractName"`
	CrossInvoke                 bool              `json:"crossInvoke"`
	CrossInvokeContractNameList []string          `json:"crossInvokeContractNameList"`
	ContractMethod              string            `json:"contractMethod"`
	ContractVersion             string            `json:"contractVersion"`
	Args                        map[string]string `json:"args,omitempty"`
	GlobalStates                map[string]string `json:"globalStates,omitempty"`
}

// responseData invoke contract response
type responseData struct {
	Logs         []string          `json:"logs,omitempty"`
	Events       []*contractEvent  `json:"events,omitempty"`
	GlobalStates map[string]string `json:"globalStates,omitempty"`
	Response     response          `json:"response"`
}

type response struct {
	Status  int32  `json:"status"`
	Message string `json:"message"`
	Payload string `json:"payload"`
}

type contractEvent struct {
	Topic           string   `json:"topic,omitempty"`
	ContractName    string   `json:"contractName,omitempty"`
	ContractVersion string   `json:"contractVersion,omitempty"`
	Data            []string `json:"data,omitempty"`
}

var sequence = util.NewSequence(0)

// InvokeAllContractHandler run contract support cross contract invoke
func InvokeAllContractHandler(w http.ResponseWriter, r *http.Request) {
	stopWatcher := util.NewDefaultStopWatcher()

	uid, err := GetUidFromSession(r)
	if err != nil {
		http.Error(w, "Forbidden", http.StatusForbidden)
	}

	stopWatcher.Start("read param")
	result := gulu.Ret.NewResult()
	defer gulu.Ret.RetResult(w, r, result)

	requestData := new(invokeContractReqFormData)
	err = util.ReadArgsTo(r.Body, requestData)
	if err != nil {
		result.Code = -1
		log.Errorf("InvokeAllContractHandler unmarshal parameter fail. err:%s", err)
		return
	}

	//read workspace
	stopWatcher.StopLastAndStartNew("read workspace")
	workspace := conf.GetUserWorkspace(uid)
	if workspace == "" {
		result.Code = -1
		log.Errorf("InvokeAllContractHandler get workspace fail. err:%s", err)
		return
	}

	//get exec dir name
	stopWatcher.StopLastAndStartNew("get exec dir name")
	execDir := conf.GetExecDirName(workspace)
	if !CanAccess(uid, execDir) {
		result.Code = -1
		log.Errorf("InvokeAllContractHandler can not access directory. uid:%s, dir:%s", uid, execDir)
		return
	}

	executableFileName := common.GetExecutableFileName(conf.Docker)

	//write args to json file
	stopWatcher.StopLastAndStartNew("add args")
	// compute address to name map
	addressToNameMp := make(map[string]string)
	for i := 0; i < len(requestData.CrossInvokeContractNameList); i++ {
		tempName := requestData.CrossInvokeContractNameList[i]
		tempAddress, tempAddressErr := util.GetContractAddressByName(tempName)
		if tempAddressErr != nil {
			log.Errorf("InvokeAllContractHandler GetContractAddressByName %s failed %s ", tempName, tempAddressErr.Error())
			continue
		}
		addressToNameMp[tempAddress] = tempName
	}
	contractAddress, contractAddressErr := util.GetContractAddressByName(requestData.ContractName)
	if contractAddressErr == nil {
		addressToNameMp[contractAddress] = requestData.ContractName
	}
	//
	nextSequence := sequence.Next()
	args := &protogo.ContractArgs{
		Name:            requestData.ContractName,
		Method:          requestData.ContractMethod,
		Version:         requestData.ContractVersion,
		Sid:             requestData.Sid,
		Seq:             nextSequence,
		LogLevel:        "DEBUG",
		Args:            requestData.Args,
		GlobalSates:     requestData.GlobalStates,
		ServerAddress:   container.SC.RpcServerAddress,
		AddressToNameMp: addressToNameMp,
	}
	err = writeArgsToJsonFile(execDir, args)
	if err != nil {
		result.Code = -1
		log.Errorf("InvokeAllContractHandler write args to file fial. uid:%s, dir:%s,args:%s", uid, execDir, args)
		return
	}

	container.SC.RpcServer.AddWaitResponseChannel(*args)

	//run contract
	stopWatcher.StopLastAndStartNew("run contract")
	cmd, err := runContract(execDir, executableFileName, uid)
	if err != nil {
		result.Code = -1
		log.Errorf("InvokeAllContractHandler run contract fail. uid:%s, dir:%s, err:%s", uid, execDir, err)
		go func() {
			bytes, err2 := cmd.CombinedOutput()
			log.Errorf("InvokeAllContractHandler run contract fail. uid:%s, dir:%s, message:%s, err:%s", uid, execDir, string(bytes), err)
			sendRunErrorToWs(uid, requestData.Sid, string(bytes))
			if err2 != nil {
				sendRunErrorToWs(uid, requestData.Sid, err.Error())
			}
		}()
		return
	} else if conf.Wide.Debug {
		bytes, err := cmd.CombinedOutput()
		log.Debugf("InvokeAllContractHandler run contract fail. uid:%s, dir:%s, message:%s, err:%s", uid, execDir, string(bytes), err)
	}

	//wait result
	stopWatcher.StopLastAndStartNew("wait result")
	contractInfo := protogo.ContractInfo{
		Name:    requestData.ContractName,
		Method:  requestData.ContractMethod,
		Version: requestData.ContractVersion,
		Sid:     requestData.Sid,
		Seq:     nextSequence,
	}
	invokeResponse, err := container.SC.RpcServer.GetResponse(time.Second*time.Duration(conf.Wide.ContractRunTimeout), &contractInfo)
	log.Debugf("InvokeAllContractHandler response. response:%s", invokeResponse)
	if err != nil {
		result.Code = -1
		go removeDockerContainerIfUserDocker(uid)
		go sendRunErrorToWs(uid, requestData.Sid, err.Error())
		log.Errorf("InvokeAllContractHandler get invoke contract response fail. uid:%s, dir:%s, err:%s", uid, execDir, err)
		return
	}

	go sendRunSuccessToWs(uid, requestData.Sid)

	result.Data = translateResponse(invokeResponse)
	stopWatcher.Stop()
	log.Debugf("InvokeAllContractHandler time cost: %s", stopWatcher.Result())
}

// runContract
func runContract(execDir string, executableFileName string, uid string) (*exec.Cmd, error) {
	var cmd *exec.Cmd
	if conf.Docker {
		log.Debugf("runContract use docker. exec dir:%s, executable file name:%s, uid:%s",
			execDir, executableFileName, uid)
		cacheContainerName := containerManager.GetCacheContainerName(uid)
		if cacheContainerName != "" {
			log.Debugf("runContract use docker, use cache container. "+
				"exec dir:%s, executable file name:%s, uid:%s, container name:%s",
				execDir, executableFileName, uid, cacheContainerName)

			cmd = exec.Command("docker", "start", cacheContainerName)
		} else {
			containerName := containerManager.NextContainerName(uid)
			cmd = exec.Command(
				"docker", "run",
				"-v", "/etc/localtime:/etc/localtime",
				"--name", containerName,
				"-d",
				"-v", execDir+":"+DockerMountPath,
				"-w", DockerMountPath,
				"--network", "host",
				"--memory", conf.Wide.DockerMemory,
				"--cpus", conf.Wide.DockerCpu,
				conf.Wide.DockerImage,
				"./"+executableFileName,
			)
			containerManager.AddContainerToPool(uid, containerName)
		}
	} else {
		log.Debugf("runContract use system. exec dir:%s, executable file name:%s, uid:%s",
			execDir, executableFileName, uid)
		cmd = exec.Command(filepath.Join(execDir, executableFileName))
		cmd.Dir = execDir
	}
	return cmd, cmd.Start()
}

func removeDockerContainerIfUserDocker(uid string) {
	if conf.Docker {
		containerManager.RemoveUserContainer(uid)
		// containerManager.AddRemoveFailedContainer(uid)
	}
}

func sendRunSuccessToWs(uid, sid string) {
	defer util.PanicLog()

	channelRetOut := map[string]interface{}{}
	channelRetOut["cmd"] = "run-done"

	wsChannel, err := GetOutputWS(sid)
	if err != nil {
		log.Warnf("build all. send run success message to ws, channel is nil. uid:%s, sid:%s", uid, sid)
		return
	}

	err = wsChannel.WriteJSON(&channelRetOut)
	if nil != err {
		log.Warnf("build all. send run success message to ws. uid:%s, sid:%s, err:%v", uid, sid, err)
	}
	wsChannel.Refresh()
}

func sendRunErrorToWs(uid, sid, data string) {
	defer util.PanicLog()

	channelRetOut := map[string]interface{}{}
	channelRetOut["cmd"] = "run"
	channelRetOut["output"] = "<span class='stderr'>" + data + "</span>"

	wsChannel, err := GetOutputWS(sid)
	if err != nil {
		log.Warnf("build all. send run error message to ws, channel is nil. uid:%s, sid:%s", uid, sid)
		return
	}

	err = wsChannel.WriteJSON(&channelRetOut)
	if nil != err {
		log.Warnf("build all. send run error message to ws. uid:%s, sid:%s, err:%v", uid, sid, err)
	}
	wsChannel.Refresh()
}

func translateResponse(invokeResponse *protogo.ExecuteResult) *responseData {
	events := make([]*contractEvent, 0, len(invokeResponse.Event))
	for _, e := range invokeResponse.Event {
		events = append(events, &contractEvent{
			Topic:           e.Topic,
			ContractName:    e.ContractName,
			ContractVersion: e.ContractVersion,
			Data:            e.Data,
		})
	}

	return &responseData{
		Logs:         invokeResponse.Logs,
		Events:       events,
		GlobalStates: invokeResponse.GlobalSates,
		Response: response{
			Status:  invokeResponse.Response.Status,
			Message: invokeResponse.Response.Message,
			Payload: invokeResponse.Response.Payload,
		},
	}
}

func writeArgsToJsonFile(dir string, args *protogo.ContractArgs) error {
	argsData, err := json.Marshal(args)
	if err != nil {
		return err
	}
	argsFile := filepath.Join(dir, "args.json")
	err = ioutil.WriteFile(argsFile, argsData, 0644)
	return err
}
